package server

import (
	"chainmaker.org/chainmaker/smarteditor/logger"
	"chainmaker.org/chainmaker/smarteditor/util"
	"context"
	"net"
	"os"
	"strings"
	"sync"
	"time"

	"github.com/pkg/errors"
	"go.uber.org/zap"
	"google.golang.org/grpc"

	"chainmaker.org/chainmaker/smarteditor/server/pb/protogo"
)

// ContractRpcServer rpc server
// when invoke contract
type ContractRpcServer struct {
	ctx     context.Context
	closeCh chan struct{}

	address string
	server  *grpc.Server

	// responseMap store invoke contract response
	responseMap sync.Map //map[string]chan(*protogo.ExecuteResult)

	log *zap.SugaredLogger
}

// NewContractRpcServer new a rpc server
func NewContractRpcServer(ctx context.Context, address string) (*ContractRpcServer, error) {
	contractRpcServer := ContractRpcServer{
		ctx:     ctx,
		address: address,
		log:     logger.Get(),
	}
	return &contractRpcServer, nil
}

// Start start rpc server
func (rs *ContractRpcServer) Start() error {
	rs.log.Infof("[ContractRpcServer Start] start contract rpc server. listen address: %s", rs.address)

	listener, err := net.Listen("tcp", rs.address)
	if err != nil {
		rs.log.Errorf("[ContractRpcServer Start]. listen address fial: address: %s, err:%s", rs.address, err)
		return errors.WithMessagef(err, "listen %s failed", rs.address)
	}

	server := grpc.NewServer(grpc.EmptyServerOption{})
	cis := &contractInvokeServer{
		rpcServer: rs,
		log:       rs.log,
	}
	protogo.RegisterContractInvokeServer(server, cis)

	go func() {
		err = server.Serve(listener)
		if err != nil {
			rs.log.Errorf("[ContractRpcServer Start] start server fail, err:%v", err)
			os.Exit(-1)
		}
	}()
	rs.server = server
	rs.waitContextCloseAsync()

	rs.log.Infof("[ContractRpcServer Start] start contract rpc server, success")
	return nil
}

//waitContextCloseAsync wait context close async
func (rs *ContractRpcServer) waitContextCloseAsync() {
	go func() {
		select {
		case <-rs.ctx.Done():
			rs.log.Infof("[ContractRpcServer waitContextClose] context done")
			rs.Close()
		case <-rs.closeCh:
			//do nothing
			rs.log.Infof("[ContractRpcServer waitContextClose] server have close, stop wait")
		}
	}()
}

// Close close rpc server
func (rs *ContractRpcServer) Close() {
	rs.log.Infof("[ContractRpcServer Close] close contract rpc server start.")
	rs.server.GracefulStop()
	rs.log.Infof("[ContractRpcServer Close] close contract rpc success.")
}

// AddWaitResponseChannel add args to server
func (rs *ContractRpcServer) AddWaitResponseChannel(contractArgs protogo.ContractArgs) {
	key := constructArgsKey(contractArgs.Name, contractArgs.Method, contractArgs.Version, contractArgs.Sid, contractArgs.Seq)
	rs.log.Debugf("[ContractRpcServer AddWaitResponseChannel] entry. contract args:%s, key:%s", contractArgs.String(), key)

	//add response chan after add args
	rs.responseMap.Store(key, make(chan *protogo.ExecuteResult, 1))
}

// addResponse add response to server
func (rs *ContractRpcServer) addResponse(executeResult *protogo.ExecuteResult) error {
	rs.log.Debugf("[ContractRpcServer addResponse] entry. contract execute result:%s", executeResult.String())

	//record log if add response to closed chan
	defer util.PanicLog()

	key := constructArgsKey(executeResult.Name, executeResult.Method, executeResult.Version, executeResult.Sid, executeResult.Seq)
	if response, ok := rs.responseMap.Load(key); ok {
		responseChan := response.(chan *protogo.ExecuteResult)
		if len(responseChan) != 0 {
			rs.log.Debugf("[ContractRpcServer addResponse] add response to chan, result has add . contract execute result:%s", executeResult.String())
			return errors.New("response has exists")
		}

		responseChan <- executeResult
		rs.log.Debugf("[ContractRpcServer addResponse] add response to chan success. contract execute result:%s", executeResult.String())
		return nil
	}

	rs.log.Debugf("[ContractRpcServer addResponse] response chan not found. key:%s", key)
	return errors.New("response chan not found")
}

// GetResponse get response from rpc server
func (rs *ContractRpcServer) GetResponse(timeOut time.Duration, contractInfo *protogo.ContractInfo) (*protogo.ExecuteResult, error) {
	rs.log.Debugf("[ContractRpcServer GetResponse] entry. contract info:%s", contractInfo.String())

	key := constructArgsKey(contractInfo.Name, contractInfo.Method, contractInfo.Version, contractInfo.Sid, contractInfo.Seq)
	if value, ok := rs.responseMap.Load(key); ok {
		timeOutTimer := time.NewTimer(timeOut)
		responseChan := value.(chan *protogo.ExecuteResult)

		//release resource
		defer rs.responseMap.Delete(key)
		defer close(responseChan)
		defer timeOutTimer.Stop()

		select {
		case <-timeOutTimer.C:
			rs.log.Warnf("[ContractRpcServer GetResponse] timeout. key:%s", key)
			return nil, errors.New("timeout")
		case executeResult := <-responseChan:
			return executeResult, nil
		}
	}

	rs.log.Debugf("[ContractRpcServer GetResponse] unexcepted code, key not fount.key:%s", key)
	return nil, errors.New("key not found")
}

// constructArgsKey construct key
func constructArgsKey(contractName, contractMethod, contractVersion, contractSid, seq string) string {
	return strings.Join([]string{contractName, contractMethod, contractSid, seq}, "_")
}
